27장. 문자열 다루기 심화
6장에서 문자열의 기초를 다뤘다.
“문자열은 불변이다”,
“len 은 바이트 수다” 같은 이야기였다.
이번 장에서는 실제 코드에서 자주 만나는
문자열 처리 작업들을 살펴본다.
표준 라이브러리의 strings 와 strconv 가 주인공이다.
목표:
strings패키지의 자주 쓰는 함수 익히기- 문자열과 숫자, bool 사이의 변환 도구 익히기
- 정규표현식이 있다는 사실과 입구 정도 알기
27.1 strings 패키지
strings 는 문자열을 검사하고 변환하는
도구 모음이다.
일상 코드에서 가장 많이 쓰는 함수들을
짧은 예제와 함께 본다.
포함 여부 검사: Contains
strings.Contains("hello world", "world") // true
strings.Contains("hello", "Xyz") // false
부분 문자열이 들어 있는지 확인한다.
시작/끝 검사: HasPrefix / HasSuffix
strings.HasPrefix("hello.go", "hello") // true
strings.HasSuffix("hello.go", ".go") // true
파일명 확장자 검사 등에 자주 쓴다.
위치 찾기: Index / LastIndex
strings.Index("hello", "l") // 2
strings.LastIndex("hello", "l") // 3
strings.Index("hello", "z") // -1 (없음)
없으면 -1 을 반환한다.
분할: Split
parts := strings.Split("a,b,c,d", ",")
// []string{"a","b","c","d"}
구분자 기준으로 잘라 슬라이스로 돌려준다.
빈 구분자("") 를 주면
글자 하나하나로 쪼개진다.
다만 멀티바이트 문자에서는 의도와 다를 수 있다.
조합: Join
strings.Join([]string{"a","b","c"}, "-")
// "a-b-c"
Split 의 반대다.
치환: Replace / ReplaceAll
strings.Replace("aaaa", "a", "b", 2) // "bbaa"
strings.ReplaceAll("aaaa", "a", "b") // "bbbb"
Replace 의 마지막 인자는
바꿀 횟수다.
-1 을 주면 전부 바꾸지만,
보통은 ReplaceAll 이 더 읽기 쉽다.
대소문자: ToUpper / ToLower
strings.ToUpper("Hello") // "HELLO"
strings.ToLower("Hello") // "hello"
영문에서는 직관대로 동작한다. 유니코드 규칙도 잘 따른다.
공백/문자 제거: TrimSpace / Trim
strings.TrimSpace(" hello \n") // "hello"
strings.Trim("--hello--", "-") // "hello"
strings.TrimLeft("--hello", "-") // "hello"
strings.TrimRight("hello--", "-") // "hello"
TrimSpace는 공백/탭/개행 등을 제거Trim은 특정 문자 집합을 제거TrimLeft/TrimRight는 한쪽만
Trim두 번째 인자는 “문자 집합” 이다."-_ "을 주면-,_, 공백을 모두 제거한다. 부분 문자열이 아니다.
반복: Repeat
strings.Repeat("ab", 3) // "ababab"
같은 문자열을 N 번 이어 붙인다.
길이 검사 도우미: Count, EqualFold
strings.Count("hello", "l") // 2
strings.EqualFold("Hello", "hello") // true (대소문자 무시)
빈도수 세기, 대소문자 무관 비교에 자주 쓴다.
27.2 strings.Builder
26장에서 본 strings.Builder 를 다시 정리한다.
문자열을 누적해 만드는 표준 도구다.
왜 쓰는가
// 안 좋은 예
s := ""
for i := 0; i < 1000; i++ {
s += "x"
}
문자열은 불변이라 매번 새 문자열이 만들어진다. 반복문 안에서 하면 비용이 빠르게 누적된다.
strings.Builder 는 내부에 버퍼를 두고
키워 가며 쌓는다.
기본 사용
var b strings.Builder
b.WriteString("Hello, ")
b.WriteString("World")
b.WriteRune('!')
fmt.Println(b.String()) // "Hello, World!"
WriteString— 문자열 한 덩어리 추가WriteRune— 유니코드 글자 하나 추가WriteByte— 바이트 하나 추가String()— 지금까지 쌓은 결과 반환
미리 용량 잡기
대략 얼마나 쓸지 알면 Grow 로 잡아 둔다.
var b strings.Builder
b.Grow(1024)
내부 버퍼 재할당 횟수가 줄어든다. 26장의 슬라이스 cap 잡기와 같은 발상이다.
strings.Builder는 복사하면 안 된다. 함수에 넘길 때는 포인터로 넘긴다.
27.3 strconv 패키지
strconv 는 문자열과 다른 기본 타입 사이의 변환을 다룬다.
“숫자를 입력 받았는데 string 으로 들어왔다” 같은 상황에서 쓴다.
정수 ↔ 문자열
가장 흔한 한 쌍은 Itoa 와 Atoi 다.
s := strconv.Itoa(42) // "42"
n, err := strconv.Atoi("42") // 42, nil
n, err = strconv.Atoi("hello") // 0, error
| 함수 | 의미 |
|---|---|
Itoa | int → string |
Atoi | string → int (에러 가능) |
이름이 옛 C 표준 라이브러리의 관례를 따른다.
더 일반적인 파싱: ParseInt / ParseFloat / ParseBool
// 진법, 비트 폭 지정 가능
n, err := strconv.ParseInt("ff", 16, 64) // 255
// 실수
f, err := strconv.ParseFloat("3.14", 64) // 3.14
// 불리언
b, err := strconv.ParseBool("true") // true
ParseInt 의 두 번째 인자가 진법이다.
0 을 주면 접두사를 보고 알아서 판단한다
(0x → 16진수, 0b → 2진수 등).
세 번째 인자는 결과의 비트 폭이다.
보통 64 를 쓴다.
더 일반적인 포매팅: FormatInt / FormatFloat
strconv.FormatInt(255, 16) // "ff"
strconv.FormatFloat(3.14, 'f', 2, 64)
// "3.14"
FormatFloat 인자의 의미:
| 인자 | 의미 |
|---|---|
| 값 | 변환할 실수 |
| 포맷 | 'f', 'e', 'g' 등 |
| 자릿수 | 소수점 아래 자리 |
| 비트 폭 | 32 또는 64 |
이스케이프 처리: Quote / Unquote
문자열을 코드에 그대로 박을 수 있는 모양으로 변환하고 싶을 때 쓴다.
strconv.Quote("hello\n") // `"hello\n"`
strconv.Unquote(`"hello\n"`) // "hello\n", nil
로그에 제어 문자가 섞인 문자열을 안전하게 찍거나, 설정 파일을 만들 때 유용하다.
Atoi vs ParseInt 언제 무엇
| 상황 | 권장 |
|---|---|
| 단순 10진수 정수만 다룬다 | Atoi |
| 16진수, 2진수 등 다양한 진법 | ParseInt |
| 결과 비트 폭을 통제하고 싶다 | ParseInt |
27.4 정규표현식 살짝
복잡한 패턴 매칭이 필요할 때
정규표현식(regexp) 을 쓴다.
Go 의 regexp 패키지가 표준이다.
간단한 사용 예
re := regexp.MustCompile(`\d+`)
re.MatchString("abc 123 def")
// true (숫자가 들어 있나?)
re.FindString("abc 123 def")
// "123" (첫 번째 매칭)
re.FindAllString("a1 b22 c333", -1)
// []string{"1","22","333"}
re.ReplaceAllString("a1 b22", "#")
// "a# b#"
MustCompile— 정규식을 미리 컴파일MatchString— 매칭 여부FindString/FindAllString— 추출ReplaceAllString— 치환
그룹 캡처
괄호로 묶으면 부분 그룹을 뽑을 수 있다.
re := regexp.MustCompile(`(\w+)@(\w+)`)
m := re.FindStringSubmatch("user@example")
// []string{"user@example","user","example"}
컴파일 비용
정규식 컴파일은 비싸다. 가능하면 패키지 변수로 한 번만 컴파일한다.
var emailRe = regexp.MustCompile(`...`)
func isEmail(s string) bool {
return emailRe.MatchString(s)
}
깊이는 별도 학습
정규표현식 자체가 한 권의 책이다.
Go 의 regexp 도 RE2 라는 별도 엔진을 쓰며,
선행/후방 탐색 같은 일부 기능이 빠져 있다.
깊이 들어갈 일이 생기면
정규표현식 입문서와 regexp 공식 문서를 함께 보는 게 좋다.
27.5 정리
strings패키지로 검사, 분할, 치환, 트리밍을 한다Contains,HasPrefix,Split,Join,ReplaceAll,TrimSpace
- 문자열 누적은
strings.Builder- 반복문 안
+연결은 피한다
- 반복문 안
strconv로 문자열과 다른 타입을 변환한다- 정수:
Atoi,Itoa,ParseInt,FormatInt - 실수:
ParseFloat,FormatFloat - bool:
ParseBool,FormatBool - 이스케이프:
Quote,Unquote
- 정수:
- 복잡한 패턴은
regexp패키지MustCompile→MatchString,FindString,ReplaceAllString- 자주 쓰는 패턴은 패키지 변수로 캐시
문자열 처리는 양이 많지만 패턴은 단순하다. “검사 → 변환 → 누적” 세 갈래로 묶어 두고 필요할 때 사전처럼 찾아 쓰면 된다.
다음 장에서는 시간을 다룬다. 파일이나 네트워크만큼 자주 쓰이는 영역이다.